<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="shortcut icon" type="image/png" href="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/assets/icon.png">

<title>Recorder H5 QuickStart: 快速入门</title>
<script>
var Tips='你可以直接将 <a target="_blank" href="https://github.com/xiangyuecn/Recorder/blob/master/QuickStart.html">/QuickStart.html</a> 文件copy到你的(https)网站中，无需其他文件，就能正常开始测试了；相比 Recorder H5 (/index.html) 这个大而全(杂乱)的demo，本文件更适合入门学习'+unescape("%uD83D%uDE04");
console.log(Tips);
</script>
</head>

<body>
<script>
var onBadCDN=function(){
	document.querySelector(".badCDN").innerHTML="cdn.jsdelivr.net可能不稳定，未能检测到js被成功加载。<br>如果页面异常，请复制本页面源码到本地测试，并且批量替换掉 https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/ 为 https://xiangyuecn.gitee.io/recorder/ 就能正常测试了";
};
</script>
<div class="badCDN" style="font-size: 50px;text-align: center;color: #f60;"></div>


<!--
【1】引入框架文件，注意自己使用时应当自己把源码clone下来，然后通过src="/src/recorder-core.js"引入，这里为了方便copy文件测试起见，使用了JsDelivr CDN，这玩意不一定能正常访问

另外：[1.1]、[1.2]可以合并为使用"/recorder.mp3.min.js"，这个文件为压缩版大幅减小文件体积，已经包含了这3个源码文件
-->

<!-- 【1.1】引入核心文件 -->
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/src/recorder-core.js" onerror="onBadCDN()"></script>

<!-- 【1.2】引入相应格式支持文件；如果需要多个格式支持，把这些格式的编码引擎js文件放到后面统统加载进来即可 -->
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/src/engine/mp3.js"></script>
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/src/engine/mp3-engine.js"></script>

<!-- 【1.3】引入可选的扩展支持项，如果不需要这些扩展功能可以不引入 -->
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/src/extensions/frequency.histogram.view.js"></script>
<script src="https://cdn.jsdelivr.net/gh/xiangyuecn/Recorder@latest/src/extensions/lib.fft.js"></script>


<!-- 【2】构建界面 -->
<div class="main">
	<div class="mainBox">
		<span style="font-size:32px;color:#f60;">Recorder H5 QuickStart: 快速入门</span>
		<a href="https://github.com/xiangyuecn/Recorder" target="_blank">GitHub</a>
		| <a href="https://gitee.com/xiangyuecn/Recorder" target="_blank">Gitee</a>
		
		<div style="padding-top:10px;color:#666">
			更多Demo：
			<a class="lb" href="https://xiangyuecn.gitee.io/recorder/" target="_blank">Recorder H5</a>
			<a class="lb" href="https://xiangyuecn.gitee.io/recorder/assets/demo-vue" target="_blank">H5 vue</a>
			<a class="lb" href="https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html" target="_blank">Demo列表(可编辑)</a>
			<a class="lb" href="https://jiebian.life/web/h5/github/recordapp.aspx" target="_blank" style="text-decoration: line-through">RecordApp</a>
			
			<a class="lb" href="https://xiangyuecn.gitee.io/recorder/assets/工具-GitHub页面历史版本访问.html#url=xiangyuecn:Recorder@1.0.20032600,/QuickStart.html" target="_blank">切换到老版本测试</a>
		</div>
	</div>
	
	<div class="mainBox">
		<!-- 按钮控制区域 -->
		<div class="pd btns">
			<div>
				<button onclick="recOpen()" style="margin-right:10px">打开录音,请求权限</button>
				<button onclick="recClose()" style="margin-right:0">关闭录音,释放资源</button>
			</div>
			
			<button onclick="recStart()">录制</button>
			<button onclick="recStop()" style="margin-right:80px">停止</button>
			
			<span style="display: inline-block;">
				<button onclick="recPause()">暂停</button>
				<button onclick="recResume()">继续</button>
			</span>
			<span style="display: inline-block;">
				<button onclick="recPlay()">播放</button>
				<button onclick="recUpload()">上传</button>
			</span>
		</div>
		
		<!-- 波形绘制区域 -->
		<div class="pd recpower">
			<div style="height:40px;width:300px;background:#999;position:relative;">
				<div class="recpowerx" style="height:40px;background:#0B1;position:absolute;"></div>
				<div class="recpowert" style="padding-left:50px; line-height:40px; position: relative;"></div>
			</div>
		</div>
		<div class="pd waveBox">
			<div style="border:1px solid #ccc;display:inline-block"><div style="height:100px;width:300px;" class="recwave"></div></div>
		</div>
	</div>
	
	<!-- 日志输出区域 -->
	<div class="mainBox">
		<div class="reclog"></div>
	</div>
</div>


<!-- 【3】实现录音逻辑 -->
<script>
var rec,wave,recBlob;
/**调用open打开录音请求好录音权限**/
var recOpen=function(){//一般在显示出录音按钮或相关的录音界面时进行此方法调用，后面用户点击开始录音时就能畅通无阻了
	rec=null;
	wave=null;
	recBlob=null;
	var newRec=Recorder({
		type:"mp3",sampleRate:16000,bitRate:16 //mp3格式，指定采样率hz、比特率kbps，其他参数使用默认配置；注意：是数字的参数必须提供数字，不要用字符串；需要使用的type类型，需提前把格式支持文件加载进来，比如使用wav格式需要提前加载wav.js编码引擎
		,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd){
			//录音实时回调，大约1秒调用12次本回调
			document.querySelector(".recpowerx").style.width=powerLevel+"%";
			document.querySelector(".recpowert").innerText=bufferDuration+" / "+powerLevel;
			
			//可视化图形绘制
			wave.input(buffers[buffers.length-1],powerLevel,bufferSampleRate);
		}
	});

	createDelayDialog(); //我们可以选择性的弹一个对话框：为了防止移动端浏览器存在第三种情况：用户忽略，并且（或者国产系统UC系）浏览器没有任何回调，此处demo省略了弹窗的代码
	newRec.open(function(){//打开麦克风授权获得相关资源
		dialogCancel(); //如果开启了弹框，此处需要取消
		
		rec=newRec;
		
		//此处创建这些音频可视化图形绘制浏览器支持妥妥的
		wave=Recorder.FrequencyHistogramView({elem:".recwave"});
		
		reclog("已打开录音，可以点击录制开始录音了",2);
	},function(msg,isUserNotAllow){//用户拒绝未授权或不支持
		dialogCancel(); //如果开启了弹框，此处需要取消
		reclog((isUserNotAllow?"UserNotAllow，":"")+"打开录音失败："+msg,1);
	});
	
	window.waitDialogClick=function(){
		dialogCancel();
		reclog("打开失败：权限请求被忽略，<span style='color:#f00'>用户主动点击的弹窗</span>",1);
	};
};



/**关闭录音，释放资源**/
function recClose(){
	if(rec){
		rec.close();
		reclog("已关闭");
	}else{
		reclog("未打开录音",1);
	};
};



/**开始录音**/
function recStart(){//打开了录音后才能进行start、stop调用
	if(rec&&Recorder.IsOpen()){
		recBlob=null;
		rec.start();
		reclog("已开始录音...");
	}else{
		reclog("未打开录音",1);
	};
};

/**暂停录音**/
function recPause(){
	if(rec&&Recorder.IsOpen()){
		rec.pause();
	}else{
		reclog("未打开录音",1);
	};
};
/**恢复录音**/
function recResume(){
	if(rec&&Recorder.IsOpen()){
		rec.resume();
	}else{
		reclog("未打开录音",1);
	};
};

/**结束录音，得到音频文件**/
function recStop(){
	if(!(rec&&Recorder.IsOpen())){
		reclog("未打开录音",1);
		return;
	};
	rec.stop(function(blob,duration){
		console.log(blob,(window.URL||webkitURL).createObjectURL(blob),"时长:"+duration+"ms");
		
		recBlob=blob;
		reclog("已录制mp3："+duration+"ms "+blob.size+"字节，可以点击播放、上传了",2);
	},function(msg){
		reclog("录音失败:"+msg,1);
	});
};









/**播放**/
function recPlay(){
	if(!recBlob){
		reclog("请先录音，然后停止后再播放",1);
		return;
	};
	var cls=("a"+Math.random()).replace(".","");
	reclog('播放中: <span class="'+cls+'"></span>');
	var audio=document.createElement("audio");
	audio.controls=true;
	document.querySelector("."+cls).appendChild(audio);
	//简单利用URL生成播放地址，注意不用了时需要revokeObjectURL，否则霸占内存
	audio.src=(window.URL||webkitURL).createObjectURL(recBlob);
	audio.play();
	
	setTimeout(function(){
		(window.URL||webkitURL).revokeObjectURL(audio.src);
	},5000);
};

/**上传**/
function recUpload(){
	var blob=recBlob;
	if(!blob){
		reclog("请先录音，然后停止后再上传",1);
		return;
	};
	
	//本例子假设使用原始XMLHttpRequest请求方式，实际使用中自行调整为自己的请求方式
	//录音结束时拿到了blob文件对象，可以用FileReader读取出内容，或者用FormData上传
	var api="https://xx.xx/test_request";
	var onreadystatechange=function(title){
		return function(){
			if(xhr.readyState==4){
				if(xhr.status==200){
					reclog(title+"上传成功",2);
				}else{
					reclog(title+"没有完成上传，演示上传地址无需关注上传结果，只要浏览器控制台内Network面板内看到的请求数据结构是预期的就ok了。", "#d8c1a0");
					
					console.error(title+"上传失败",xhr.status,xhr.responseText);
				};
			};
		};
	};
	reclog("开始上传到"+api+"，请求稍后...");

	/***方式一：将blob文件转成base64纯文本编码，使用普通application/x-www-form-urlencoded表单上传***/
	var reader=new FileReader();
	reader.onloadend=function(){
		var postData="";
		postData+="mime="+encodeURIComponent(blob.type);//告诉后端，这个录音是什么格式的，可能前后端都固定的mp3可以不用写
		postData+="&upfile_b64="+encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result)||[])[1]) //录音文件内容，后端进行base64解码成二进制
		//...其他表单参数
		
		var xhr=new XMLHttpRequest();
		xhr.open("POST", api);
		xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
		xhr.onreadystatechange=onreadystatechange("上传方式一【Base64】");
		xhr.send(postData);
	};
	reader.readAsDataURL(blob);

	/***方式二：使用FormData用multipart/form-data表单上传文件***/
	var form=new FormData();
	form.append("upfile",blob,"recorder.mp3"); //和普通form表单并无二致，后端接收到upfile参数的文件，文件名为recorder.mp3
	//...其他表单参数
	
	var xhr=new XMLHttpRequest();
	xhr.open("POST", api);
	xhr.onreadystatechange=onreadystatechange("上传方式二【FormData】");
	xhr.send(form);
};










//recOpen我们可以选择性的弹一个对话框：为了防止移动端浏览器存在第三种情况：用户忽略，并且（或者国产系统UC系）浏览器没有任何回调
var showDialog=function(){
	if(!/mobile/i.test(navigator.userAgent)){
		return;//只在移动端开启没有权限请求的检测
	};
	dialogCancel();
	
	//显示弹框，应该使用自己的弹框方式
	var div=document.createElement("div");
	document.body.appendChild(div);
	div.innerHTML=(''
		+'<div class="waitDialog" style="z-index:99999;width:100%;height:100%;top:0;left:0;position:fixed;background:rgba(0,0,0,0.3);">'
			+'<div style="display:flex;height:100%;align-items:center;">'
				+'<div style="flex:1;"></div>'
				+'<div style="width:240px;background:#fff;padding:15px 20px;border-radius: 10px;">'
					+'<div style="padding-bottom:10px;">录音功能需要麦克风权限，请允许；如果未看到任何请求，请点击忽略~</div>'
					+'<div style="text-align:center;"><a onclick="waitDialogClick()" style="color:#0B1">忽略</a></div>'
				+'</div>'
				+'<div style="flex:1;"></div>'
			+'</div>'
		+'</div>');
};
var createDelayDialog=function(){
	dialogInt=setTimeout(function(){//定时8秒后打开弹窗，用于监测浏览器没有发起权限请求的情况，在open前放置定时器利于收到了回调能及时取消（不管open是同步还是异步回调的）
		showDialog();
	},8000);
};
var dialogInt;
var dialogCancel=function(){
	clearTimeout(dialogInt);
	
	//关闭弹框，应该使用自己的弹框方式
	var elems=document.querySelectorAll(".waitDialog");
	for(var i=0;i<elems.length;i++){
		elems[i].parentNode.removeChild(elems[i]);
	};
};
//recOpen弹框End
</script>








<!--以下这坨可以忽略-->
<script>
function reclog(s,color){
	var now=new Date();
	var t=("0"+now.getHours()).substr(-2)
		+":"+("0"+now.getMinutes()).substr(-2)
		+":"+("0"+now.getSeconds()).substr(-2);
	var div=document.createElement("div");
	var elem=document.querySelector(".reclog");
	elem.insertBefore(div,elem.firstChild);
	div.innerHTML='<div style="color:'+(!color?"":color==1?"red":color==2?"#0b1":color)+'">['+t+']'+s+'</div>';
};
window.onerror=function(message, url, lineNo, columnNo, error){
	//https://www.cnblogs.com/xianyulaodi/p/6201829.html
	reclog('<span style="color:red">【Uncaught Error】'+message+'<pre>'+"at:"+lineNo+":"+columnNo+" url:"+url+"\n"+(error&&error.stack||"不能获得错误堆栈")+'</pre></span>');
};

reclog("RecordApp[即将废弃] 除Recorder支持的外，支持Hybrid App，低版本IOS上支持微信网页和小程序web-view"+unescape("%uD83C%uDF89"),"#f60;font-weight:bold;font-size:24px");
reclog("Recorder H5使用简单，功能丰富，支持PC、Android、IOS 14.3+"+unescape("%uD83D%uDCAA"),"#0b1;font-weight:bold;font-size:24px");
reclog('本页面修改时间（有可能修改了忘改）：2022-03-03 21:48:53',"#999");
reclog('Recorder库修改时间（有可能修改了忘改）：'+Recorder.LM,"#999");
reclog(Tips);
</script>

<script>
if(/mobile/i.test(navigator.userAgent)){
	//移动端加载控制台组件
	var elem=document.createElement("script");
	elem.setAttribute("type","text/javascript");
	elem.setAttribute("src","https://xiangyuecn.gitee.io/recorder/assets/ztest-vconsole.js");
	document.body.appendChild(elem);
	elem.onload=function(){
		new VConsole();
	};
};
</script>

<!-- 加载打赏挂件 -->
<script src="https://xiangyuecn.gitee.io/recorder/assets/zdemo.widget.donate.js"></script>
<script>
var donateView=document.createElement("div");
document.querySelector(".reclog").appendChild(donateView);
DonateWidget({
	log:function(msg){reclog(msg)}
	,mobElem:donateView
});
</script>

<style>
body{
	word-wrap: break-word;
	background:#f5f5f5 center top no-repeat;
	background-size: auto 680px;
}
pre{
	white-space:pre-wrap;
}
a{
	text-decoration: none;
	color:#06c;
}
a:hover{
	color:#f00;
}

.main{
	max-width:700px;
	margin:0 auto;
	padding-bottom:80px
}

.mainBox{
	margin-top:12px;
	padding: 12px;
	border-radius: 6px;
	background: #fff;
	--border: 1px solid #f60;
	box-shadow: 2px 2px 3px #aaa;
}


.btns button{
	display: inline-block;
	cursor: pointer;
	border: none;
	border-radius: 3px;
	background: #f60;
	color:#fff;
	padding: 0 15px;
	margin:3px 20px 3px 0;
	line-height: 36px;
	height: 36px;
	overflow: hidden;
	vertical-align: middle;
}
.btns button:active{
	background: #f00;
}

.pd{
	padding:0 0 6px 0;
}
.lb{
	display:inline-block;
	vertical-align: middle;
	background:#00940e;
	color:#fff;
	font-size:14px;
	padding:2px 8px;
	border-radius: 99px;
}
</style>

</body>
</html>